Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for connection pool monitoring #702

Open
wants to merge 19 commits into
base: master
Choose a base branch
from

Conversation

karpetrosyan
Copy link
Member

@karpetrosyan karpetrosyan commented May 29, 2023

closes #508

@karpetrosyan karpetrosyan changed the title closes #508 Add support for connection pool monitoring May 29, 2023
@karpetrosyan karpetrosyan added the enhancement New feature or request label May 29, 2023
@karpetrosyan
Copy link
Member Author

karpetrosyan commented May 30, 2023

These trace events were added.

  • connection_pool.reuse_connection when our request received the idle connection from the pool
  • connection_pool.set_connection when our request received the connection which was just created
  • connection_pool.unset_connection when the connection pool steals our request's connection

We can use an extension's trace to set a callback for these events because they are traced along with the request argument.

Additionally, some events do not support callbacks.

These are events that are unrelated to requests.

  • connection_pool.remove_idle_connection pool removes the idle connection
  • connection_pool.create_connection pool creates the connection
  • connection_pool.remove_expired_connection pool removes the expired connection

@tomchristie
Copy link
Member

Could you demo some log output as part of the description here?

@karpetrosyan
Copy link
Member Author

script

import asyncio
import logging

import httpcore

logging.basicConfig(
    format="%(levelname)s [%(asctime)s] %(name)s - %(message)s",
    datefmt="%Y-%m-%d %H:%M:%S",
)
logging.getLogger("httpcore.connection_pool").setLevel(logging.DEBUG)

async def main():
    async with httpcore.AsyncConnectionPool() as pool:
        tasks = []
        for i in range(5):
            tasks.append(pool.request('GET', 'http://encode.io'))
        await asyncio.gather(*tasks)

asyncio.run(main())

OUTPUT

DEBUG [2023-05-30 16:00:10] httpcore.connection_pool - create_connection.started origin='http://encode.io:80'
DEBUG [2023-05-30 16:00:10] httpcore.connection_pool - create_connection.complete return_value=<AsyncHTTPConnection [CONNECTING]>
DEBUG [2023-05-30 16:00:10] httpcore.connection_pool - set_connection.started connection=<AsyncHTTPConnection [CONNECTING]>
DEBUG [2023-05-30 16:00:10] httpcore.connection_pool - set_connection.complete
DEBUG [2023-05-30 16:00:10] httpcore.connection_pool - Waiting for a connection.
DEBUG [2023-05-30 16:00:10] httpcore.connection_pool - The ConnectionPool currently has 1 request(s) awaiting connections.
DEBUG [2023-05-30 16:00:10] httpcore.connection_pool - Connection received.
DEBUG [2023-05-30 16:00:10] httpcore.connection_pool - The ConnectionPool currently has 0 request(s) awaiting connections.
DEBUG [2023-05-30 16:00:10] httpcore.connection_pool - create_connection.started origin='http://encode.io:80'
DEBUG [2023-05-30 16:00:10] httpcore.connection_pool - create_connection.complete return_value=<AsyncHTTPConnection [CONNECTING]>
DEBUG [2023-05-30 16:00:10] httpcore.connection_pool - set_connection.started connection=<AsyncHTTPConnection [CONNECTING]>
DEBUG [2023-05-30 16:00:10] httpcore.connection_pool - set_connection.complete
DEBUG [2023-05-30 16:00:10] httpcore.connection_pool - Waiting for a connection.
DEBUG [2023-05-30 16:00:10] httpcore.connection_pool - The ConnectionPool currently has 1 request(s) awaiting connections.
DEBUG [2023-05-30 16:00:10] httpcore.connection_pool - Connection received.
DEBUG [2023-05-30 16:00:10] httpcore.connection_pool - The ConnectionPool currently has 0 request(s) awaiting connections.
DEBUG [2023-05-30 16:00:10] httpcore.connection_pool - create_connection.started origin='http://encode.io:80'
DEBUG [2023-05-30 16:00:10] httpcore.connection_pool - create_connection.complete return_value=<AsyncHTTPConnection [CONNECTING]>
DEBUG [2023-05-30 16:00:10] httpcore.connection_pool - set_connection.started connection=<AsyncHTTPConnection [CONNECTING]>
DEBUG [2023-05-30 16:00:10] httpcore.connection_pool - set_connection.complete
DEBUG [2023-05-30 16:00:10] httpcore.connection_pool - Waiting for a connection.
DEBUG [2023-05-30 16:00:10] httpcore.connection_pool - The ConnectionPool currently has 1 request(s) awaiting connections.
DEBUG [2023-05-30 16:00:10] httpcore.connection_pool - Connection received.
DEBUG [2023-05-30 16:00:10] httpcore.connection_pool - The ConnectionPool currently has 0 request(s) awaiting connections.
DEBUG [2023-05-30 16:00:10] httpcore.connection_pool - create_connection.started origin='http://encode.io:80'
DEBUG [2023-05-30 16:00:10] httpcore.connection_pool - create_connection.complete return_value=<AsyncHTTPConnection [CONNECTING]>
DEBUG [2023-05-30 16:00:10] httpcore.connection_pool - set_connection.started connection=<AsyncHTTPConnection [CONNECTING]>
DEBUG [2023-05-30 16:00:10] httpcore.connection_pool - set_connection.complete
DEBUG [2023-05-30 16:00:10] httpcore.connection_pool - Waiting for a connection.
DEBUG [2023-05-30 16:00:10] httpcore.connection_pool - The ConnectionPool currently has 1 request(s) awaiting connections.
DEBUG [2023-05-30 16:00:10] httpcore.connection_pool - Connection received.
DEBUG [2023-05-30 16:00:10] httpcore.connection_pool - The ConnectionPool currently has 0 request(s) awaiting connections.
DEBUG [2023-05-30 16:00:10] httpcore.connection_pool - create_connection.started origin='http://encode.io:80'
DEBUG [2023-05-30 16:00:10] httpcore.connection_pool - create_connection.complete return_value=<AsyncHTTPConnection [CONNECTING]>
DEBUG [2023-05-30 16:00:10] httpcore.connection_pool - set_connection.started connection=<AsyncHTTPConnection [CONNECTING]>
DEBUG [2023-05-30 16:00:10] httpcore.connection_pool - set_connection.complete
DEBUG [2023-05-30 16:00:10] httpcore.connection_pool - Waiting for a connection.
DEBUG [2023-05-30 16:00:10] httpcore.connection_pool - The ConnectionPool currently has 1 request(s) awaiting connections.
DEBUG [2023-05-30 16:00:10] httpcore.connection_pool - Connection received.
DEBUG [2023-05-30 16:00:10] httpcore.connection_pool - The ConnectionPool currently has 0 request(s) awaiting connections.
DEBUG [2023-05-30 16:00:11] httpcore.connection_pool - Connection: <AsyncHTTPConnection ['http://encode.io:80', HTTP/1.1, IDLE, Request Count: 1]> was returned into the pool
DEBUG [2023-05-30 16:00:11] httpcore.connection_pool - Connection: <AsyncHTTPConnection ['http://encode.io:80', HTTP/1.1, IDLE, Request Count: 1]> was returned into the pool
DEBUG [2023-05-30 16:00:11] httpcore.connection_pool - Connection: <AsyncHTTPConnection ['http://encode.io:80', HTTP/1.1, IDLE, Request Count: 1]> was returned into the pool
DEBUG [2023-05-30 16:00:11] httpcore.connection_pool - Connection: <AsyncHTTPConnection ['http://encode.io:80', HTTP/1.1, IDLE, Request Count: 1]> was returned into the pool
DEBUG [2023-05-30 16:00:11] httpcore.connection_pool - Connection: <AsyncHTTPConnection ['http://encode.io:80', HTTP/1.1, IDLE, Request Count: 1]> was returned into the pool

@karpetrosyan
Copy link
Member Author

With the trace extention enabled

import asyncio
import httpcore
async def log(a, b):
    if a.startswith('connection_pool'):
        print(a, b)
async def main():
    async with httpcore.AsyncConnectionPool(max_connections=1) as pool:
        tasks = []
        for i in range(5):
            tasks.append(pool.request('GET', 'http://encode.io', extensions={'trace': log}))
        await asyncio.gather(*tasks)
asyncio.run(main())

OUTPUT

connection_pool.set_connection.started {'connection': <AsyncHTTPConnection [CONNECTING]>}
connection_pool.set_connection.complete {'return_value': None}
connection_pool.reuse_connection.started {'connection': <AsyncHTTPConnection ['http://encode.io:80', HTTP/1.1, IDLE, Request Count: 1]>}
connection_pool.reuse_connection.complete {'return_value': None}
connection_pool.reuse_connection.started {'connection': <AsyncHTTPConnection ['http://encode.io:80', HTTP/1.1, IDLE, Request Count: 1]>}
connection_pool.reuse_connection.complete {'return_value': None}
connection_pool.reuse_connection.started {'connection': <AsyncHTTPConnection ['http://encode.io:80', HTTP/1.1, IDLE, Request Count: 1]>}
connection_pool.reuse_connection.complete {'return_value': None}
connection_pool.reuse_connection.started {'connection': <AsyncHTTPConnection ['http://encode.io:80', HTTP/1.1, IDLE, Request Count: 1]>}
connection_pool.reuse_connection.complete {'return_value': None}
connection_pool.unset_connection.started {'connection': <AsyncHTTPConnection ['http://encode.io:80', HTTP/1.1, ACTIVE, Request Count: 2]>}
connection_pool.unset_connection.complete {'return_value': None}
connection_pool.unset_connection.started {'connection': <AsyncHTTPConnection ['http://encode.io:80', HTTP/1.1, ACTIVE, Request Count: 2]>}
connection_pool.unset_connection.complete {'return_value': None}
connection_pool.unset_connection.started {'connection': <AsyncHTTPConnection ['http://encode.io:80', HTTP/1.1, ACTIVE, Request Count: 2]>}
connection_pool.unset_connection.complete {'return_value': None}
connection_pool.reuse_connection.started {'connection': <AsyncHTTPConnection ['http://encode.io:80', HTTP/1.1, IDLE, Request Count: 2]>}
connection_pool.reuse_connection.complete {'return_value': None}
connection_pool.reuse_connection.started {'connection': <AsyncHTTPConnection ['http://encode.io:80', HTTP/1.1, IDLE, Request Count: 2]>}
connection_pool.reuse_connection.complete {'return_value': None}
connection_pool.reuse_connection.started {'connection': <AsyncHTTPConnection ['http://encode.io:80', HTTP/1.1, IDLE, Request Count: 2]>}
connection_pool.reuse_connection.complete {'return_value': None}
connection_pool.unset_connection.started {'connection': <AsyncHTTPConnection ['http://encode.io:80', HTTP/1.1, ACTIVE, Request Count: 3]>}
connection_pool.unset_connection.complete {'return_value': None}
connection_pool.unset_connection.started {'connection': <AsyncHTTPConnection ['http://encode.io:80', HTTP/1.1, ACTIVE, Request Count: 3]>}
connection_pool.unset_connection.complete {'return_value': None}
connection_pool.reuse_connection.started {'connection': <AsyncHTTPConnection ['http://encode.io:80', HTTP/1.1, IDLE, Request Count: 3]>}
connection_pool.reuse_connection.complete {'return_value': None}
connection_pool.reuse_connection.started {'connection': <AsyncHTTPConnection ['http://encode.io:80', HTTP/1.1, IDLE, Request Count: 3]>}
connection_pool.reuse_connection.complete {'return_value': None}
connection_pool.unset_connection.started {'connection': <AsyncHTTPConnection ['http://encode.io:80', HTTP/1.1, ACTIVE, Request Count: 4]>}
connection_pool.unset_connection.complete {'return_value': None}
connection_pool.reuse_connection.started {'connection': <AsyncHTTPConnection ['http://encode.io:80', HTTP/1.1, IDLE, Request Count: 4]>}
connection_pool.reuse_connection.complete {'return_value': None}

@tomchristie
Copy link
Member

Wonderful - thanks for working through this.

We should avoid adding extra state as part of this change, so I'd suggest we avoid the .self._requests_awaiting_connection.

It seems to me that the bits we'd like to trace don't fit in as well with the .started/.complete pairs that we're using elsewhere. Perhaps we want to trace these just as single events instead?

Maybe we'd implement that with a def trace() function for single event cases, instead of the Trace class?

The bits we want to monitor are state changes to ._pool and ._requests.

Suppose we started just with the following trace events/debug lines...

DEBUG [2023-06-09 10:55:09] httpcore.connection_pool - add_connection <HTTPConnection [...]>
DEBUG [2023-06-09 10:55:09] httpcore.connection_pool - add_request <Request [...]>
DEBUG [2023-06-09 10:55:09] httpcore.connection_pool - assign_request_to_connection <Request [...]> <HTTPConnection >
DEBUG [2023-06-09 10:55:09] httpcore.connection_pool - unassign_request_from_connection <Request [...]> <HTTPConnection [...]>
DEBUG [2023-06-09 10:55:09] httpcore.connection_pool - timeout_waiting_for_connection <Request [...]>
DEBUG [2023-06-09 10:55:09] httpcore.connection_pool - remove_request <Request [...]>
DEBUG [2023-06-09 10:55:09] httpcore.connection_pool - remove_connection <HTTPConnection [...]>

Would that be sufficiently comprehensive?

@karpetrosyan
Copy link
Member Author

I've added all of the events you mentioned, as well as the "reuse_connection" event.

This is how it looks like now

connection_pool.add_request {'request': <Request [b'GET']>}
connection_pool.add_connection {'connection': <AsyncHTTPConnection [CONNECTING]>}
connection_pool.assign_request_to_connection {'request': <Request [b'GET']>, 'connection': <AsyncHTTPConnection [CONNECTING]>}
connection_pool.remove_request {'request': <Request [b'GET']>}
connection_pool.add_request {'request': <Request [b'GET']>}
connection_pool.reuse_connection {'connection': <AsyncHTTPConnection ['https://www.encode.io:443', HTTP/1.1, IDLE, Request Count: 1]>, 'request': <Request [b'GET']>}
connection_pool.assign_request_to_connection {'request': <Request [b'GET']>, 'connection': <AsyncHTTPConnection ['https://www.encode.io:443', HTTP/1.1, IDLE, Request Count: 1]>}
connection_pool.remove_request {'request': <Request [b'GET']>}
connection_pool.add_request {'request': <Request [b'GET']>}
connection_pool.reuse_connection {'connection': <AsyncHTTPConnection ['https://www.encode.io:443', HTTP/1.1, IDLE, Request Count: 2]>, 'request': <Request [b'GET']>}

@karpetrosyan
Copy link
Member Author

Also timeout event

connection_pool.timeout_waiting_for_connection {'request': <Request [b'GET']>}

Copy link
Member

@tomchristie tomchristie left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is looking really promising...
Here's some initial suggestions.

docs/extensions.md Outdated Show resolved Hide resolved
docs/extensions.md Outdated Show resolved Hide resolved
docs/extensions.md Show resolved Hide resolved
httpcore/_async/connection_pool.py Outdated Show resolved Hide resolved
tests/_async/test_connection_pool.py Outdated Show resolved Hide resolved
@DoryZi
Copy link

DoryZi commented Apr 3, 2024

What's the status of this PR?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Add connection pool DEBUG logs & tracing.
4 participants